package com.awol2010ex.logback.appender.common;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * Adapt from XMLLayout
 */
public class JSONEventLayout {

    String source;
    String sourceHost;
    String sourcePath;
    List<String> tags;
    List<AdditionalField> additionalFields;
    String type;
    private DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    private boolean locationInfo = false;
    private int callerStackIdx = 0;
    private boolean properties = false;


    public JSONEventLayout() {
        try {
            setSourceHost(InetAddress.getLocalHost().getHostName());
        } catch (UnknownHostException e) {
        }
    }


    /**
     * Returns the current value of the <b>LocationInfo</b> option.
     */
    public boolean getLocationInfo() {
        return locationInfo;
    }

    /**
     * The <b>LocationInfo</b> option takes a boolean value. By default, it is
     * set to false which means there will be no location information output by
     * this layout. If the the option is set to true, then the file name and
     * line number of the statement at the origin of the log statement will be
     * output.
     *
     * <p>
     * If you are embedding this layout within an
     * <code>org.apache.log4j.net.SMTPAppender</code> then make sure to set the
     * <b>LocationInfo</b> option of that appender as well.
     */
    public void setLocationInfo(boolean flag) {
        locationInfo = flag;
    }

    /**
     * Gets whether MDC key-value pairs should be output.
     *
     * @return true if MDC key-value pairs are output.
     * @since 1.2.15
     */
    public boolean getProperties() {
        return properties;
    }

    /**
     * Sets whether MDC key-value pairs should be output, default false.
     *
     * @param flag new value.
     * @since 1.2.15
     */
    public void setProperties(final boolean flag) {
        properties = flag;
    }

    private String nullToEmpty(String v) {
        return v==null?"":v;
    }
    public synchronized Map<String, String> doLayout(ILoggingEvent event) {
        Map<String, String> ret =new HashMap<>();
        ret.put("source",nullToEmpty(source));
        ret.put("host",nullToEmpty(sourceHost));
        ret.put("path",nullToEmpty(sourcePath));
        ret.put("type",nullToEmpty(type));
        ret.put("tags",tags==null?"": StrUtil.join(",",tags));
        ret.put("message",event.getFormattedMessage());
        ret.put("timestamp",df.format(new Date(event.getTimeStamp())));
        ret.put("logger",event.getLoggerName());
        ret.put("level",event.getLevel().toString());
        ret.put("thread",event.getThreadName());

        IThrowableProxy tp = event.getThrowableProxy();
        if (tp != null) {
            String throwable = ThrowableProxyUtil.asString(tp);
            ret.put("throwable", throwable);
        }
        if (locationInfo) {
            StackTraceElement[] callerDataArray = event.getCallerData();
            if (callerDataArray != null
                    && callerDataArray.length > callerStackIdx) {
                StackTraceElement immediateCallerData = callerDataArray[callerStackIdx];
                ret.put("location-class", immediateCallerData.getClassName());
                ret.put("location-method", immediateCallerData.getMethodName());
                ret.put("location-file", immediateCallerData.getFileName());
                ret.put("location-line", Integer.toString(immediateCallerData.getLineNumber()));
            }
        }

        if (properties) {
            Map<String, String> propertyMap = event.getMDCPropertyMap();
            if ((propertyMap != null) && (propertyMap.size() != 0)) {
                Set<Entry<String, String>> entrySet = propertyMap.entrySet();


                Iterator<Entry<String, String>> i = entrySet.iterator();
                while (i.hasNext()) {
                    Entry<String, String> entry = i.next();
                    ret.put("properties-" + entry.getKey(), entry.getValue());

                }
            }
        }

        if (additionalFields != null) {
            for (AdditionalField field : additionalFields) {

                ret.put(field.getKey(), field.getValue());
            }
        }
        return ret;
    }

    public String getContentType() {
        return "application/json";
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public String getSourceHost() {
        return sourceHost;
    }

    public void setSourceHost(String sourceHost) {
        this.sourceHost = sourceHost;
    }

    public String getSourcePath() {
        return sourcePath;
    }

    public void setSourcePath(String sourcePath) {
        this.sourcePath = sourcePath;
    }

    public List<String> getTags() {
        return tags;
    }

    public void setTags(List<String> tags) {
        this.tags = tags;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getCallerStackIdx() {
        return callerStackIdx;
    }

    /**
     * Location information dump with respect to call stack level. Some
     * framework (Play) wraps the original logging method, and dumping the
     * location always log the file of the wrapper instead of the actual caller.
     * For PlayFramework, I use 2.
     *
     * @param callerStackIdx
     */
    public void setCallerStackIdx(int callerStackIdx) {
        this.callerStackIdx = callerStackIdx;
    }

    public void addAdditionalField(AdditionalField p) {
        if (additionalFields == null) {
            additionalFields = new ArrayList<AdditionalField>();
        }
        additionalFields.add(p);
    }

}

